#  Utils.py -- Handy boilerplate code for ProctorTicket
#  Copyright (C) 2008 Zachary Voase <cracka80@gmail.com>
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
#  02110-1301, USA.

"""ProctorTicket.Utils
Contains some of the functions and classes used by both halves of the
ProctorTicket system, including some functions missing from the Python standard
library, like popd and pushd, and a filtered list."""

from os import getcwdu, chdir, linesep
import operator
import re

class FilteredList( list ):
    """FilteredList(<filterFunction: lambda x: True>, *args, **kwargs):
A list which can only have items appended to it which pass the 'filterFunction'
callable attribute, like an incremental 'filter' function. Handy in the addition
of results to a results list, whilst filtering them at the same time instead of
having to use intermediate lists and unnecessary memory. By default, the
filtering function returns True every time. The remaining arguments will be
passed to list.__init__() as entered."""
    
    def __init__( self, filterFunction=lambda x: True, *args, **kwargs ):
        list.__init__( self, *args, **kwargs )
        self.filterFunction = filterFunction
    
    def append( self, *args, **kwargs ):
        """FilteredList.append(item):
Modified list.append method: Only adds the item to list if it returns True from
the callable attribute 'filterFunction', specified upon instantiation. Whether
or not the value passes, it behaves like the normal list.append function (i.e.
it returns None)."""
        if self.filterFunction( *args, **kwargs ):
            list.append( self, *args, **kwargs )

_dirstack = [ getcwdu() ]

def pushd( path ):
    """pushd(<string: path>):
Push current directory onto directory stack, chdir to specified path."""
    _dirstack.append( unicode( path ) )
    chdir( path )

def popd():
    """popd():
Remove current directory from stack, chdir back to last visited path."""
    _dirstack.pop()
    chdir( _dirstack[-1] )

def dirs():
    """dirs():
Returns concatenated string of directory stack."""
    return " ".join( _dirstack )

_RESULT_DICT_DEFAULTS = {
    'type': 'defect', 
    'component': 'general', 
    'priority': 'normal', 
    'status': 'new'
}

def dictFilter( function, dictIn ):
    dictOut = {}
    for key, value in dictIn.items():
        if function( key, value ):
            dictOut[key] = value
    return dictOut

def dictEmptiness( key, value ):
    if value in [ "", u"", 0, None, [], (), 0.0, {} ]:
        return False
    return True

def DefaultSummariser( resultslist, **kwargs ):
    """DefaultSummariser(<list: proctorlib.result.TestResult>, **kwargs):
The default summarising action takes a list of results and returns
the test class to which those results belong. The results should
already have been grouped accordingly by their test class, so this
function will use the test class of the first item of the list, 
appended by '(proctor)', which should be unique enough for the
summary field to be distinctive.
Example: 'proctorlib.tests.ExampleTestCase (proctor)'"""
    return GetTestClass( resultslist[0] ) + ' (proctor)'

def ResultEquality( result1, result2 ):
    if not GetTestClass( result1 ) == GetTestClass( result2 ):
        return False
    elif not result1.status == result2.status:
        return False
    if result1.output == result2.output:
        return True
    output1 = result1.output.split( linesep, 1 )
    output2 = result2.output.split( linesep, 1 )
    if output1[1] == output2[1]:
        return True
    return False

def list_contains( listin, item, eq=operator.eq ):
    """list_contains(<list>, item, eq=operator.eq):
Returns True/False depending on whether the item, passed as the second
positional argument, is in the list, passed as the first positional
argument, according to the comparison operator, passed as the 'eq'
keyword argument.
    """
    for x in listin:
        if eq(item, x):
            return True
    return False

def UniquifyResults( listin ):
    """UniquifyResults(<list: proctorlib.result.TestResult>):
Returns a list of results from the results list where duplicates have been
removed. This is done by testing for equality between the output,
status and test class of the tests, which is handled by the ResultEquality
function, and deleting one if they are equal."""
    listout = []
    for item in listin:
        if not list_contains( listout, item, eq=ResultEquality ):
            listout.append(item)
    return listout

def GetFuncName( result ):
    """GetFuncName(<proctorlib.result.TestResult>):
Returns the specific function name of a test result."""
    return result.name.split( "." )[-1]

def GetTestClass( result ):
    """GetTestClass(<proctorlib.result.TestResult>):
Takes a TestResult and returns the test class it belongs to, by removing the
last name from the dotted module name held in its 'name' attribute."""
    return ".".join( result.name.split( " " )[1].split( "." )[:-1] )

def EscapeCamel( stringIn ):
    return re.sub( 
    r"(^|\b)(?P<text>([A-Z][a-z]{1,}){2,})(\b|$)", 
    r"!\g<text>", 
    stringIn )
        

def DefaultDescriptor( results ):
    """DefaultDescriptor(<list: proctorlib.result.TestResult>):
Takes a list of results for use in a ticket and generate a description which
contains the output for each test in the list, in Trac wiki markup. They are
separated by a line of 20 hyphen ('-') characters, and so can be split up
using str.split."""
    listOut = []
    for i in range( len( results ) ):
        result = results[i]
        subList = []
        if i is not 0:
            subList.append( "-"*20 )
        subList.append( "=== %s ===" % ( EscapeCamel( result.name ), ) )
        subList.append( "{{{" )
        subList.append( result.output )
        subList.append( "}}}" )
        listOut.append( linesep.join( subList ) )
    return linesep.join( listOut )

def Results2TicketDict( results, summariser=DefaultSummariser, descriptor=DefaultDescriptor, **kwargs ):
    """Results2TicketDict(<proctorlib.result.TestResult>, <summariser: DefaultSummariser>, <descriptor: DefaultDescriptor>, **kwargs):
Takes a list of TestResult objects, and some keyword arguments, and returns a dictionary
containing the valid field names for the Trac ticket table. The 'summariser'
keyword argument specifies a function which produces a string to be used as the
unique ticket summary. This will receive the TestResult list as the only argument.
The 'descriptor' keyword receives the list of results also and is responsible for
the generation of the description of the ticket in Trac.
By specifying extra keyword arguments, you can change the values of the ticket
fields. Extra keywords must be valid according to the field list held in the list
ProctorTicket.Trac._TICKET_FIELDS."""
    dictOut = {
        'id':0, 
        'type': "", 
        'time':0, 
        'changetime':0, 
        'component': "", 
        'severity': "", 
        'priority': "", 
        'owner': "", 
        'reporter': "", 
        'cc': "", 
        'version': "", 
        'milestone': "", 
        'status': "", 
        'resolution': "", 
        'summary': "", 
        'description': "", 
        'keywords': []
    }
    dictOut["summary"] = summariser( results )
    dictOut["description"] = descriptor( results )
    for key, value in kwargs.items():
        if dictOut.has_key( key ) and isinstance( value, dictOut.get( key, "" ).__class__ ):
            dictOut[key] = value
    return dictFilter( dictEmptiness, dictOut )
